/*
 * Rakp3.java 
 * Created on 2011-07-26
 *
 * Copyright (c) Verax Systems 2011.
 * All rights reserved.
 *
 * This software is furnished under a license. Use, duplication,
 * disclosure and all other uses are restricted to the rights
 * specified in the written license agreement.
 */
package com.veraxsystems.vxipmi.coding.commands.session;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import com.veraxsystems.vxipmi.coding.commands.IpmiCommandCoder;
import com.veraxsystems.vxipmi.coding.commands.IpmiVersion;
import com.veraxsystems.vxipmi.coding.commands.ResponseData;
import com.veraxsystems.vxipmi.coding.payload.CompletionCode;
import com.veraxsystems.vxipmi.coding.payload.IpmiPayload;
import com.veraxsystems.vxipmi.coding.payload.PlainMessage;
import com.veraxsystems.vxipmi.coding.payload.lan.IPMIException;
import com.veraxsystems.vxipmi.coding.payload.lan.NetworkFunction;
import com.veraxsystems.vxipmi.coding.protocol.AuthenticationType;
import com.veraxsystems.vxipmi.coding.protocol.IpmiMessage;
import com.veraxsystems.vxipmi.coding.protocol.Ipmiv20Message;
import com.veraxsystems.vxipmi.coding.protocol.PayloadType;
import com.veraxsystems.vxipmi.coding.protocol.encoder.Protocolv20Encoder;
import com.veraxsystems.vxipmi.coding.security.CipherSuite;
import com.veraxsystems.vxipmi.coding.security.ConfidentialityNone;
import com.veraxsystems.vxipmi.coding.security.SecurityConstants;
import com.veraxsystems.vxipmi.common.TypeConverter;

/**
 * A wrapper for RMCP+ RAKP3 message and it's response - RAKP4 message.
 */
public class Rakp3 extends IpmiCommandCoder {

	/**
	 * Status of the previous message.
	 */
	private byte statusCode;

	/**
	 * The Managed System's Session ID for this session. Must be as returned by
	 * the Managed System in the Open Session Response message.
	 */
	private int managedSystemSessionId;

	private Rakp1 rakp1;

	private Rakp1ResponseData rakp1ResponseData;

	public void setStatusCode(byte statusCode) {
		this.statusCode = statusCode;
	}

	public byte getStatusCode() {
		return statusCode;
	}

	public void setManagedSystemSessionId(int managedSystemSessionId) {
		this.managedSystemSessionId = managedSystemSessionId;
	}

	public int getManagedSystemSessionId() {
		return managedSystemSessionId;
	}

	/**
	 * Initiates class for decoding. Sets IPMI version to
	 * {@link IpmiVersion#V20} since RAKP1 is a RMCP+ command. Sets
	 * Authentication Type to RMCP+.
	 * 
	 * @param cipherSuite
	 *            - {@link CipherSuite} containing authentication,
	 *            confidentiality and integrity algorithms for this session.
	 *            Only authentication algorithm is used at this point of
	 *            creating a session.
	 * @param rakp1
	 *            - RAKP Message 1 sent earlier in the authentification process
	 * @param rakp1ResponseData
	 *            - RAKP Message 2 received earlier in the authentification
	 *            process
	 * 
	 */
	public Rakp3(CipherSuite cipherSuite, Rakp1 rakp1,
			Rakp1ResponseData rakp1ResponseData) {
		super(IpmiVersion.V20, cipherSuite, AuthenticationType.RMCPPlus);
		this.rakp1 = rakp1;
		this.rakp1ResponseData = rakp1ResponseData;
		setCipherSuite(new CipherSuite((byte) 0, cipherSuite
				.getAuthenticationAlgorithm().getCode(), (byte) 0, (byte) 0));
	}

	/**
	 * Initiates class for encoding and decoding. Sets IPMI version to
	 * {@link IpmiVersion#V20} since RAKP1 is a RMCP+ command. Sets
	 * Authentication Type to RMCP+.
	 * 
	 * @param statusCode
	 *            - Status of the previous message.
	 * @param managedSystemSessionId
	 *            - The Managed System's Session ID for this session. Must be as
	 *            returned by the Managed System in the Open Session Response
	 *            message.
	 * @param cipherSuite
	 *            - {@link CipherSuite} containing authentication,
	 *            confidentiality and integrity algorithms for this session.
	 *            Only authentication algorithm is used at this point of
	 *            creating a session.
	 * @param rakp1
	 *            - RAKP Message 1 sent earlier in the authentification process
	 * @param rakp1ResponseData
	 *            - RAKP Message 2 received earlier in the authentification
	 *            process
	 */
	public Rakp3(byte statusCode, int managedSystemSessionId,
			CipherSuite cipherSuite, Rakp1 rakp1,
			Rakp1ResponseData rakp1ResponseData) {
		super(IpmiVersion.V20, cipherSuite, AuthenticationType.RMCPPlus);
		setStatusCode(statusCode);
		setManagedSystemSessionId(managedSystemSessionId);
		this.rakp1 = rakp1;
		this.rakp1ResponseData = rakp1ResponseData;
		setCipherSuite(new CipherSuite((byte) 0, cipherSuite
				.getAuthenticationAlgorithm().getCode(), (byte) 0, (byte) 0));
	}

	@Override
	public IpmiMessage encodeCommand(int sequenceNumber, int sessionId)
			throws NoSuchAlgorithmException, InvalidKeyException {

		if (sessionId != 0) {
			throw new IllegalArgumentException("Session ID must be 0");
		}

		Ipmiv20Message message = new Ipmiv20Message(new ConfidentialityNone());

		message.setPayloadType(PayloadType.Rakp3);
		message.setSessionID(0);
		message.setSessionSequenceNumber(0);
		message.setAuthenticationType(getAuthenticationType());
		message.setPayloadAuthenticated(getCipherSuite()
				.getIntegrityAlgorithm().getCode() != SecurityConstants.IA_NONE);
		message.setPayloadEncrypted(false);

		message.setPayload(preparePayload(sequenceNumber));

		message.setAuthCode(getCipherSuite()
				.getIntegrityAlgorithm()
				.generateAuthCode(
						message.getIntegrityAlgorithmBase(new Protocolv20Encoder())));

		return message;
	}

	@Override
	protected IpmiPayload preparePayload(int sequenceNumber)
			throws NoSuchAlgorithmException, InvalidKeyException {
		byte[] payload = new byte[8];

		payload[0] = TypeConverter.intToByte(sequenceNumber % 256); // message
																	// tag

		payload[1] = getStatusCode(); // last message status code

		payload[2] = 0; // reserved
		payload[3] = 0; // reserved

		byte[] manSesId = TypeConverter
				.intToLittleEndianByteArray(getManagedSystemSessionId());

		System.arraycopy(manSesId, 0, payload, 4, 4); // managed system session
														// id

		byte[] exchangeAuthCode = getCipherSuite().getAuthenticationAlgorithm()
				.getKeyExchangeAuthenticationCode(
						prepareKeyExchangeAuthenticationCodeBase(rakp1,
								rakp1ResponseData), rakp1.getPassword());

		byte[] result = null;

		if (exchangeAuthCode != null) {
			result = new byte[8 + exchangeAuthCode.length];
			System.arraycopy(exchangeAuthCode, 0, result, 8,
					exchangeAuthCode.length);
		} else {
			result = new byte[8];
		}

		System.arraycopy(payload, 0, result, 0, 8);

		return new PlainMessage(result);
	}

	/**
	 * @return byte array holding prepared base for calculating
	 *         KeyExchangeAuthenticationCode for RAKP Message 3
	 */
	private byte[] prepareKeyExchangeAuthenticationCodeBase(Rakp1 rakp1,
			Rakp1ResponseData responseData) {
		int length = 22;
		if (rakp1.getUsername() != null) {
			length += rakp1.getUsername().length();
		}
		byte[] keac = new byte[length];

		System.arraycopy(responseData.getManagedSystemRandomNumber(), 0, keac,
				0, 16);

		System.arraycopy(TypeConverter.intToLittleEndianByteArray(responseData
				.getRemoteConsoleSessionId()), 0, keac, 16, 4);

		keac[20] = TypeConverter.intToByte(encodePrivilegeLevel(rakp1
				.getRequestedMaximumPrivilegeLevel()) | 0x10);

		if (rakp1.getUsername() != null) {
			keac[21] = TypeConverter.intToByte(rakp1.getUsername().length());
			if (rakp1.getUsername().length() > 0) {
				System.arraycopy(rakp1.getUsername().getBytes(), 0, keac, 22,
						rakp1.getUsername().length());
			}
		} else {
			keac[21] = 0;
		}

		return keac;
	}

	@Override
	@Deprecated
	public byte getCommandCode() {
		return 0;
	}

	@Override
	@Deprecated
	public NetworkFunction getNetworkFunction() {
		return null;
	}

	@Override
	public ResponseData getResponseData(IpmiMessage message)
			throws IllegalArgumentException, IPMIException,
			InvalidKeyException, NoSuchAlgorithmException {

		if (!isCommandResponse(message)) {
			throw new IllegalArgumentException("This is not RAKP 4 message!");
		}

		byte[] payload = message.getPayload().getPayloadData();

		Rakp3ResponseData data = new Rakp3ResponseData();

		data.setMessageTag(payload[0]);

		data.setStatusCode(payload[1]);

		if (payload[1] != 0) {
			throw new IPMIException(CompletionCode.parseInt(TypeConverter
					.byteToInt(payload[1])));
		}

		if (payload.length < 8) {
			throw new IllegalArgumentException("Invalid payload length");
		}

		byte[] buffer = new byte[4];

		System.arraycopy(payload, 4, buffer, 0, 4);

		data.setConsoleSessionId(TypeConverter
				.littleEndianByteArrayToInt(buffer));

		byte[] integrityCheck = null;

		if (payload.length > 8) {
			integrityCheck = new byte[getCipherSuite()
					.getAuthenticationAlgorithm().getIntegrityCheckBaseLength()];
			System.arraycopy(payload, 8, integrityCheck, 0, getCipherSuite()
					.getAuthenticationAlgorithm().getIntegrityCheckBaseLength());
		}

		if (!getCipherSuite().getAuthenticationAlgorithm().doIntegrityCheck(
				prepareIntegrityCheckBase(rakp1, rakp1ResponseData),
				integrityCheck, rakp1.calculateSik(rakp1ResponseData))) {
			throw new IllegalArgumentException("Integrity check failed");
		}

		return data;
	}

	/**
	 * @return byte array holding prepared base for calculating Integrity Check
	 */
	private byte[] prepareIntegrityCheckBase(Rakp1 rakp1,
			Rakp1ResponseData responseData) {
		byte[] icb = new byte[36];

		System.arraycopy(rakp1.getConsoleRandomNumber(), 0, icb, 0, 16);

		System.arraycopy(TypeConverter.intToLittleEndianByteArray(rakp1
				.getManagedSystemSessionId()), 0, icb, 16, 4);

		System.arraycopy(responseData.getManagedSystemGuid(), 0, icb, 20, 16);

		return icb;
	}

    @Override
    public boolean isCommandResponse(IpmiMessage message) {
        return message instanceof Ipmiv20Message && ((Ipmiv20Message) message).getPayloadType() == PayloadType.Rakp4;
    }

}
